#include <Python.h>
#include <comedilib.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <errno.h>
#include <unistd.h>
#include "ezdaq.h"

#define MAX_CHANNELS 64

static comedi_t *dev; //the comedi device
static int streaming_buffer_size; //size of the analog input buffer
static int ai_subdev; //analog input subdevice
static int dio_subdev; //digital i/o subdevice
static void *map; //the comedi device is mapped to this location in memory
static unsigned int analog_channel_list[MAX_CHANNELS]; //the list of all the channels
static int analog_channel_list_len;
static comedi_range *analog_ranges[MAX_CHANNELS];
static lsampl_t analog_maxes[MAX_CHANNELS];
static comedi_cmd analog_cmd, *analog_command=&analog_cmd;
static unsigned int back = 0;
static unsigned int front = 0;

//method declarations:
static PyMethodDef methods[] = {
  {"configure_device",configure_device, METH_VARARGS, "Generic Device Configuration. Opens the comedi device."},  
  {"configure_dio", configure_dio, METH_VARARGS, "Configure the digital i/o channels"},  
  {"configure_streaming", configure_streaming, METH_VARARGS, "Configure the analog streaming channels"},  
  {"start_streaming", start_streaming, METH_VARARGS, "Start the analog streaming."},  
  {"stop_streaming", stop_streaming, METH_VARARGS, "Stop the analog streaming."},  
  {"set_bit", set_bit, METH_VARARGS, "Set the bit on a given channel"},
  {"get_results", get_results, METH_VARARGS, "Get the acquisition results"},  
  {"close_device",close_device, METH_VARARGS, "Close the comedi device."},  
  {NULL, NULL,0,NULL}
};

PyMODINIT_FUNC initezdaq(){   
    (void)Py_InitModule("ezdaq", methods); 
}

/**
 * Configure the comedi device.  Everyone should call this first BEFORE
 * executing more specific configuration functions like confgiure_dio()
 * or configure_streaming().
 */
static PyObject* configure_device(PyObject *self, PyObject *args) {
  PyObject *dict, *deviceStringObject;
  char* deviceString;
  PyArg_ParseTuple(args, "O",  &dict);
  deviceStringObject = PyDict_GetItemString(dict, "device");
  deviceString = PyString_AsString(deviceStringObject);

  //open the device
  dev = comedi_open(deviceString);
  if(!dev) {
    PyErr_SetString(PyExc_IOError, "Could not open device!");
  }

  return getSuccessObject();
}

/**
 * Configure the digital I/O channels.
 */
static PyObject* configure_dio(PyObject *self, PyObject *args) {
  PyObject *dict, *digital_channel_list;
  int digital_channel_list_length, i;

  PyArg_ParseTuple(args, "O",  &dict);

  digital_channel_list = PyDict_GetItemString(dict, "digital");
  digital_channel_list_length = PyList_Size(digital_channel_list);

  dio_subdev = comedi_find_subdevice_by_type(dev, COMEDI_SUBD_DIO, 0);

  /* the dict["digital"] returns a list of lists.  The top list has one 
     entry for each channel, the lowest list has one entry representing 
     the channel number and another entry representing the channel type:
     0 for input and 1 for output.  We configure comedi accordingly.
   */
  for(i=0;i<digital_channel_list_length;i++) {
    PyObject *channel_info_list = PyList_GetItem(digital_channel_list, i);
    int channel = (int) PyInt_AsLong(PyList_GetItem(channel_info_list, 0));
    char *type = PyString_AsString(PyList_GetItem(channel_info_list, 1));
    if(strcmp(type, "COMEDI_INPUT")==0) {
      comedi_dio_config(dev, dio_subdev, channel, COMEDI_INPUT);
    } else if(strcmp(type, "COMEDI_OUTPUT")==0){
      comedi_dio_config(dev, dio_subdev, channel, COMEDI_OUTPUT);
    } else {
      PyErr_SetString(PyExc_IOError, "Invalid digital i/o type.");
      return getNullObject();
    }
  }
  
  return getSuccessObject();
}

static PyObject* configure_streaming(PyObject *self, PyObject *args) {
  PyObject *dict, *analog_channel_list_obj;
  int i;

  PyArg_ParseTuple(args, "O",  &dict);

  analog_channel_list_obj = PyDict_GetItemString(dict, "analog");
  analog_channel_list_len = PyList_Size(analog_channel_list_obj);

  ai_subdev = comedi_find_subdevice_by_type(dev, COMEDI_SUBD_AI, 0);
  streaming_buffer_size = comedi_get_buffer_size(dev, ai_subdev);

  //map the device to memory for faster reads:
  map=mmap(NULL,streaming_buffer_size,PROT_READ,MAP_SHARED,comedi_fileno(dev),0);
  if( map == NULL ){
    PyErr_SetString(PyExc_IOError, "Could not complete mmap!");
  }

  for(i=0;i<analog_channel_list_len;i++) {
    PyObject *channel_info_list = PyList_GetItem(analog_channel_list_obj, i);
    int channel = (int) PyInt_AsLong(PyList_GetItem(channel_info_list, 0));
    int range   = (int) PyInt_AsLong(PyList_GetItem(channel_info_list, 1));
    char *aref = PyString_AsString(PyList_GetItem(channel_info_list , 2));
    if(strcmp(aref, "AREF_GROUND")) {
      analog_channel_list[i]=CR_PACK(channel, range, AREF_GROUND);
    } else if(strcmp(aref, "AREF_COMMON")) {
      analog_channel_list[i]=CR_PACK(channel, range, AREF_COMMON);
    } else if(strcmp(aref, "AREF_DIFF")) {
      analog_channel_list[i]=CR_PACK(channel, range, AREF_DIFF);
    } else {
      PyErr_SetString(PyExc_IOError, "Invalid streaming analog type.");
      return getNullObject();
    }
    analog_maxes[i] = comedi_get_maxdata(dev, ai_subdev, channel); 
    analog_ranges[i] = comedi_get_range(dev, ai_subdev, channel, range);
  }

  //prepare the command for continuous acquisition:
  int period_ns = PyInt_AsLong(PyDict_GetItemString(dict, "analog_period_ns"));
  comedi_get_cmd_generic_timed(dev, ai_subdev, analog_command, period_ns);
  analog_command->scan_end_arg = analog_channel_list_len;
	
  //continuous acquisition
  analog_command->stop_src = TRIG_NONE;
  analog_command->stop_arg = 0;

  analog_command->chanlist = analog_channel_list;
  analog_command->chanlist_len = analog_channel_list_len;

  //validate the command
  int isValid = comedi_command_test(dev, analog_command);
  if(isValid<0) {
    isValid = comedi_command_test(dev, analog_command);
    if(isValid<0) {
      printf("Second command not valid.\n");
      PyErr_SetString(PyExc_IOError, "Analog streaming command not valid.");
      return getNullObject();
    }
  }
  return getSuccessObject();
}

static PyObject * start_streaming(PyObject *self, PyObject *args) {
  int ret = comedi_command(dev, analog_command);
  if(ret<0) {
    
    PyErr_SetString(PyExc_IOError, "Could not execute command!");
    return getNullObject();
  } 

  return getSuccessObject();
}

static PyObject * stop_streaming(PyObject *self, PyObject *args) {
  comedi_cancel(dev, ai_subdev);
  return getSuccessObject();
}

static PyObject *set_bit(PyObject *self, PyObject *args) {
  int channel, bit;
  PyArg_ParseTuple(args, "ii", &channel, &bit);
  int ret = comedi_dio_write(dev, dio_subdev, channel, bit);
  return getSuccessObject();
}

static PyObject * get_results(PyObject *self, PyObject *args) {
  int i, ret;
  front += comedi_get_buffer_contents(dev,ai_subdev);
  if(front==back) {
    return Py_BuildValue("");
  } else if(front<back){
    PyErr_SetString(PyExc_IOError, "Front was less than back.");
    return Py_BuildValue("");
  }

  //Create two NEW lists to store the newly read in items:
  //this is how big we need to make each of our lists
  //2 represents the number of channels we have
  int listsize = (front-back)/(analog_channel_list_len*sizeof(sampl_t));

  PyObject *new_lists[MAX_CHANNELS];
  int list_indices[MAX_CHANNELS];

  for(i=0;i<analog_channel_list_len;i++) {
    new_lists[i]  = PyList_New(listsize);
    list_indices[i]=0;
  }

  /* This loop goes through the read in items, casts them, and puts them
   * in the proper array.
   */
  for(i=back;i<front;i+=sizeof(sampl_t)){
    static int chan = 0;
    sampl_t read = *(sampl_t *)(map+(i%streaming_buffer_size));
    double read_double = (double)comedi_to_phys(read,analog_ranges[chan],analog_maxes[chan]);
    PyList_SetItem(new_lists[chan],list_indices[chan],PyFloat_FromDouble(read_double));
    list_indices[chan]++;
    chan++;
    if(chan==analog_channel_list_len) {
      chan = 0;
    }
  }
    
  ret = comedi_mark_buffer_read(dev,ai_subdev,front-back);
  if(ret<0){
    comedi_perror("comedi_mark_buffer_read");
    PyErr_SetString(PyExc_IOError, "Comedi marking error.");
    return Py_BuildValue("");
  }
  front = front%streaming_buffer_size;
  back = front;
  PyObject *return_list = PyList_New(analog_channel_list_len);
  for(i=0;i<analog_channel_list_len;i++) {
    PyList_SetItem(return_list, i, new_lists[i]);
  }

  return Py_BuildValue("O", return_list);
}

static PyObject * close_device(PyObject *self, PyObject *args) {
  front = 0;
  back = 0;
  comedi_close(dev);
  return getSuccessObject();
}

//Utility functions:
static PyObject* getSuccessObject() {
  return Py_BuildValue("i", 1);
}

static PyObject* getNullObject() {
  return Py_BuildValue("");
}
